<!-- eslint-disable -->
<template>
  <div
    ref="box"
    class="box-wrapper"
  >
    <template v-for="(vy, ky) in matrix">
      <template v-for="(vo, kx) in vy">
        <template v-if="(vo instanceof Object)">
          <!-- 组件 1x1 -->
          <Card
            :key="`s1-${ky}-${kx}-${vo.id}`"
            :style="vo.styles"
            :class="vo.classes"
            :info="boxInfos[vo.id] || {}"
            :postion="vo"
            @click.native="onExpand(vo)"
          />
        </template>
      </template>
    </template>
  </div>
</template>

<script>
/* eslint-disable */
import Card from './Card.vue';

/** 默认配置 */
const defaultOptions = {
  /** 区域大小 单位: 个 */
  wSize: 5,
  hSize: 4,
  /** 间隙宽占比, 高占比 单位: px */
  wGapSize: 20,
  hGapSize: 20,
  /** 暂停时间 单位: 秒 */
  stopTime: 10,
  /** 发牌时间 单位: 秒 */
  refreshTime: 15,
  /** 组件类型配置 */
  boxConfig: [
    {
      start: 9, // 起始数量
      limit: 9, // 限制最多多少
      refresh: 2, // 每次刷新增加多少
      size: { ws: 1, hs: 1 }, // 组件大小
    }, // 1x1 // 11
    {
      start: 3,
      limit: 3,
      refresh: 1,
      size: { ws: 2, hs: 1 },
    }, // 2x1 // 2
  ],
  // 初始接口 > 用于获取初始数据 : 返回 Promise
  netInit: null,
  // 刷新接口 > 用于获取刷新数据 : 返回 Promise
  netRefresh: null,
};

export default {
  components: { Card },
  props: {
    options: { type: Object, default: () => ({}) },
  },

  data() {
    return {
      /** 组件配置 */
      config: {},

      /**
       * 渲染区域
       * 多维数组 [[{...}, ...], ...]
       * 例如 5 x 4 的大小, 其中 { size } 代表渲染对象 1 代表对象占位, 0 代表空占位
       * 其中 { size: 6 } 代表占位 3x2 大小组件, 记作 {6}
       * 其中 { size: 2 } 代表占位 2x1 大小组件, 记作 {2}
       * 其中 { size: 1 } 代表占位 1x1 大小组件, 记作 {1}
       * [ 
       *   [{6}, 1, 1, {1}, {1}],
       *   [  1, 1, 1, {2},   1],
       *   [{2}, 1, 0,   0,   0],
       *   [ 0,  0, 0,   0,   0],
       * ]
       */
      matrix: [], // 映射网格: [ 行索引: [ ...(组件|占位|空缺) ], ... ]

      /**
       * 组件相关
       * 单个组件属性说明
       * {
       *  id: 1,                     // 组件ID, 全局标识用于缓存
       *  xi: 0, yi: 0,              // 坐标x,y
       *  ws: 1, hs: 1,              // 宽高w,h
       *  origin: { ws: 1, hs: 1 },  // 初始大小w,h
       *  position: ['0,0', '1,0'],  // 占用坐标点
       *  info: { ... },             // 更多信息，用于组件内部渲染
       * }
       */
      boxInfos: {}, // 组件信息映射 {id => {...}, ...}
      boxActive: null, // 选中组件
      boxNumbers: {}, // 当前组件数量 { '${宽}x${高}': <数量>, ... }

      /** 定时器句柄 */
      stopTimer: null,

      /** 刷新器句柄 */
      refreshTimer: null,

      /** 标识递增值 */
      idIndex: 0,

      /** 动画缓存 */
      animateCache: {},
    };
  },

  watch: {
    options: {
      immediate: true,
      handler() {
        /** 合并配置 */
        this.config = {
          ...JSON.parse(JSON.stringify(defaultOptions)),
          ...this.options,
          ...{
            /** 基础宽高 无需配置, 自动计算 */
            wBasis: 0,
            hBasis: 0,
            /** 区域宽高 无需配置, 自动计算 */
            wOffset: 0,
            hOffset: 0,
          },
        };

        /** 重置标识 */
        this.idIndex = 0;

        /** 加载出局 */
        this.netGetInit().then((boxs) => {
          if (this.$refs.box) this.onInit(boxs);
          else this.$nextTick(() => this.onInit(boxs));
        });
      }
    },
  },

  methods: {
    /** 事件# 初始化 */
    onInit(boxs) {
      const {
        offsetWidth,
        offsetHeight,
      } = this.$refs.box;
      const {
        wSize,
        hSize,
        wGapSize,
        hGapSize,
        refreshTime,
      } = this.config;
      // 计算实际大小
      this.config.wOffset = offsetWidth;
      this.config.hOffset = offsetHeight;
      // 计算基础宽高
      this.config.wBasis = (offsetWidth - ((wSize - 1) * wGapSize)) / wSize;
      this.config.hBasis = (offsetHeight - ((hSize - 1) * hGapSize)) / hSize;
      // 创建网格
      const matrix = this.makeMatrix();
      // 出现动画
      const animationName = this.animationShow();
      // 增加动画
      boxs.forEach((box) => box.classes = [animationName]);
      // 默认填充
      this.fillMatrix(matrix, boxs);
      // 空行填充
      this.fillBlank(matrix);
      // 渲染网格
      this.matrix = matrix;
      // 启动刷新器
      this.refreshTimer && clearInterval(this.refreshTimer);
      this.refreshTimer = setInterval(() => this.onRefresh(), refreshTime * 1000);
    },

    /** 事件# 定时刷新 */
    onRefresh() {
      if (this.boxActive) return; // 正在放大不予刷新
      this.netGetRefresh().then((boxsRefresh) => {
        const { hSize, wSize } = this.config;

        // 获取所有组件
        const boxsAll = this.getBoxs();

        // 获取需要删除
        const deleteds = this.getDeletes();

        // 获取新增动画
        const animationName = this.animationShow();

        // 记录原来位置
        const boxsOrigin = JSON.parse(JSON.stringify(boxsAll));

        console.log('删除的', deleteds.join(', '));

        // 创建新的网格
        const matrix = this.makeMatrix();

        // 移除组件渲染
        boxsAll.forEach(box => {
          // /** 移除组件 */
          // if (deleteds.includes(box.id)) return;
          /** 移除动画 */
          box.classes = [];

          /** 原样渲染 */
          this.fillBox(matrix, box);
        });

        // 增加动画
        boxsRefresh.forEach((box) => box.classes = [animationName]);

        console.log('新增的', boxsRefresh.length);

        // 从后往前渲染
        const nots = this.fillMatrix(matrix, boxsRefresh, false, false);

        // 移除组件渲染
        for (let yi = 0; yi < hSize; yi ++) {
          for (let xi = 0; xi < wSize; xi ++) {
            if (
              matrix[yi][xi] instanceof Object
              && deleteds.includes(matrix[yi][xi].id)
            ) this.clearBox(matrix, matrix[yi][xi]);
          }
        }

        // 正序渲染
        if (nots.length > 0) this.fillMatrix(matrix, nots);

        console.log('丢失的', nots.length);

        // 移除组件数据
        this.delBox(deleteds);

        // 获取新的位置
        const boxsTarget = this.getBoxs(matrix);

        // 如果坐标变动（增加移动动画）
        boxsTarget.forEach((iBox) => {

          /** 获取坐标改变 */
          let item = boxsOrigin.find(v => v.id === iBox.id);
          if (
            !item
            || (item.xi === iBox.xi && item.yi === iBox.yi)
          ) return;

          /** 设置移动动画 */
          iBox.classes = [this.animationMove(item.styles, iBox.styles)];
        });

        // 渲染网格
        this.matrix = matrix;
      });
    },

    /** 事件# 点击组件放大3x2 */
    onExpand(box, { nw = 3, nh = 2 } = {}) {
      // 正在放大不予放大
      if (box.id === this.boxActive?.id) return;

      // 缩小组件（历史放大的）
      this.zoomOutBox();

      // 放大组件（当前的组件）
      this.boxActive = box;
      this.zoomInBox(box, { nw, nh });

      // 创建网格
      const matrix = this.makeMatrix();

      // 填充组件（当前组件）
      this.fillBox(matrix, box);

      // 获取当前组件（位置顺序信息）
      const boxs = this.getBoxs();

      // 记录原来位置
      const boxsOrigin = JSON.parse(JSON.stringify(boxs));

      // 按原坐标填充（排除当前组件）（获取被占用的位置组件）
      const holds = [];
      boxs.forEach((vo) => {
        /** 忽略自身 */
        if (vo.id === box.id) return;

        /** 移除动画（出现） */
        vo.classes = [];

        /** 忽略占用 */
        if (box.postions.some(p => vo.postions.includes(p))) {
          holds.push(vo);
        } else {
          this.fillBox(matrix, vo);
        }
      });

      // 顺序填充（排除当前组件）
      this.fillMatrix(matrix, holds, true);

      // 空行填充
      this.fillBlank(matrix);

      // 获取新的位置
      const boxsTarget = this.getBoxs(matrix);

      // 如果坐标变动（增加移动动画）
      boxsTarget.forEach((iBox) => {
        let item = boxsOrigin.find(v => v.id === iBox.id);
        if (!item) return; // 不存在
        if (item.id === box.id) return; // 等于当前放大的组件
        if (item.xi === iBox.xi && item.yi === iBox.yi) return; // 坐标未改变

        /** 设置移动动画 */
        iBox.classes = [this.animationMove(item.styles, iBox.styles)];
      });

      // 渲染新的网格
      this.matrix = matrix;

      // 超时后缩小组件
      const { stopTime } = this.config;
      this.stopTimer && clearTimeout(this.stopTimer);
      this.stopTimer = setTimeout(() => this.zoomOutBox(), stopTime * 1000);
    },

    /** 网络# 获取更新组件数据 */
    async netGetRefresh() {
      const {
        netRefresh,
        boxConfig,
      } = this.config;

      // 未配置返回空
      if (!netRefresh) return [];

      // 封装请求参数
      const reqConfig = [];
      boxConfig.forEach(({ size, refresh }) => {
        reqConfig.push({ ...size, number: refresh });
      });

      // 等待返回结果
      const result = await netRefresh(reqConfig);

      // 为结果添加id, ws, hs, origin
      const packages = [];
      result.forEach(({ size, data }) => {
        this.boxNumbers[`${size.ws}x${size.hs}`] += data.length;
        data.forEach((info) => {
          packages.push(this.makeBox(info, size));
        });
      });

      // 随机排序
      packages.sort(() => Math.random() - 0.5);

      // 返回结果
      return packages;
    },

    /** 网络# 获取初始组件数据 */
    async netGetInit() {
      const {
        netInit,
        boxConfig,
      } = this.config;

      // 设置当前数量
      this.boxNumbers = {};

      // 未配置返回空
      if (!netInit) return [];

      // 封装请求参数
      const reqConfig = [];
      boxConfig.forEach(({ size, start }) => {
        reqConfig.push({ ...size, number: start });
      });

      // 等待返回结果
      const result = await netInit(reqConfig);

      // 为结果添加id, ws, hs, origin
      const packages = [];
      result.forEach(({ size, data }) => {
        this.boxNumbers[`${size.ws}x${size.hs}`] = data.length;
        data.forEach((info) => {
          packages.push(this.makeBox(info, size));
        });
      });

      // 随机排序
      packages.sort(() => Math.random() - 0.5);

      // 返回结果
      return packages;
    },

    /** 辅助# 获取删除 */
    getDeletes() {
      const { boxConfig } = this.config;
      const all = this.getBoxs();

      // 对组件进行分组
      const boxGroup = {};
      all.forEach((box) => {
        let { id, ws, hs } = box;
        let key = `${ws}x${hs}`;
        if (boxGroup[key]) boxGroup[key].push(id);
        else boxGroup[key] = [id];
      });

      // 对分组进行排序（总是移除id最小的）
      Object.keys(boxGroup).forEach((key) => {
        boxGroup[key].sort((a, b) => a - b);
      });

      // 计算移除的组件
      const deleteds = [];
      Object.keys(boxGroup).forEach((key) => {
        let iConfig = boxConfig.find(({ size }) => {
          return `${size.ws}x${size.hs}` === key;
        });
        if (this.boxNumbers[key] > iConfig.limit) {
          // 获取指定数量移除组件
          for (let i = 0; i < (this.boxNumbers[key] - iConfig.limit); i ++) deleteds.push(boxGroup[key][i]);
          // 设置移除后的数量大小
          this.boxNumbers[key] = iConfig.limit;
        }
      });
      return deleteds;
    },

    /** 辅助# 获取标识 */
    genUuid() {
      this.idIndex += 1;
      return this.idIndex;
    },

    /** 辅助# 获取组件 */
    getBoxs(matrix = null) {
      const boxs = [];
      const { wSize, hSize } = this.config;

      if (!matrix) matrix = this.matrix;

      for (let yi = 0; yi < hSize; yi ++) {
        for (let xi = 0; xi < wSize; xi ++) {
          if (matrix[yi][xi] instanceof Object) boxs.push(matrix[yi][xi]);
        }
      }
      return boxs;
    },

    /** 辅助# 放大组件 */
    zoomInBox(box, { nw, nh } = {}) {
      const { wSize, hSize } = this.config;

      // 检测到超出边界
      if (nw > wSize && nh > hSize) return;

      // 获取原来的位置
      const origin = { ...box.styles };

      // 自适应边界大小
      box.xi = (box.xi + nw) > wSize ? (wSize - nw) : box.xi;
      box.yi = (box.yi + nh) > hSize ? (hSize - nh) : box.yi;
      box.ws = nw; box.hs = nh;
      box.postions = this.getPostions(box);

      // 计算放大后位置
      this.styleBox(box);

      // 获取计算后位置
      const target = { ...box.styles };

      // 增加放大的动画
      box.classes = [this.animationActive(origin, target)];
    },

    /** 辅助# 缩小组件 */
    zoomOutBox() {
      if (!this.boxActive) return;
      const box = this.boxActive; this.boxActive = null;
      box.ws = box.origin.ws;
      box.hs = box.origin.hs;
      box.postions = [];
      box.classes = [];
      this.fillBox(this.matrix, box);
    },

    /** 辅助# 顺序填充 */
    fillMatrix(matrix, nodes, sort = false, specially = true) {
      const temps = []; // 暂存未分配的组件
      const { wSize, hSize } = this.config;

      // 是否从大到小排序
      if (sort) nodes.sort((a, b) => (b.ws * b.hs) - (a.ws * a.hs));

      // 正序填充
      for (let yi = 0; yi < hSize; yi ++) {
        for (let xi = 0; xi < wSize; xi ++) {
          // 占用跳过
          if (matrix[yi][xi] !== 0) continue;
          // 开始分配
          while (nodes.length > 0) {
            let box = nodes.shift();
            box.xi = xi; box.yi = yi;
            if (this.canBox(matrix, box)) {
              // 准入分配
              this.fillBox(matrix, box);
              break;
            } else {
              // 回收分配
              temps.push(box);
            }
          }
          // 加入回收
          temps.forEach(() => nodes.push(temps.shift()));
        }
      }

      // 特殊处理
      const nots = [];
      if (specially) {
        nodes.forEach(box => {
          if (!this.fillSpecially(matrix, box)) nots.push(box);
        });
      } else {
        nodes.forEach(box => nots.push(box));
      }

      return nots;
    },

    /** 辅助# 特殊填充 */
    fillSpecially(matrix, box) {
      /**
       * 【特殊处理：仅针对 1x1, 2x1 样本情况下有用】
       * 被剩余的一定是组件 2x1 的，来个特殊处理
       * 从后往前找到 (1x1相邻1x1) 或 (1x1相邻null) 剔除后
       * 加入2x1到该位置中去
       * 最后再执行一次从头替换到脚把剔除的1x1填充过去
       * 发现经过排序后不太可能会出现这种情况，再考虑看看（大组件和小组件各50%会丢失）
       * 还是要处理一遭，经过处理还是丢失就丢了吧
       */
      const { wSize, hSize } = this.config;

      // 获取1x1组件集合
      const iBox1x1 = this.getBoxs().filter(v => v.ws === 1 && v.hs === 1);
      
      // 获取1x1相邻组件
      let nears = null;
      for (let i = 0; i < iBox1x1.length; i ++) {
        nears = this.getNears(matrix, iBox1x1[i]);
        if (nears) break;
      }

      // 加入2x1到该位置
      if (nears) {

        // 清空1x1
        nears.nears.forEach(ibox => this.clearBox(matrix, ibox));

        // 填充2x1
        box.xi = nears.point.xi;
        box.yi = nears.point.yi;
        this.fillBox(matrix, box);

        // 填充1x1（从头开始）
        for (let yi = 0; yi < hSize; yi ++) {
          for (let xi = 0; xi < wSize; xi ++) {

            // 跳过空集
            if (nears.nears.length === 0) break;

            // 跳过占用
            if (matrix[yi][xi] !== 0) continue;

            // 加入组件
            let ibox = nears.nears.shift();
            ibox.xi = xi; ibox.yi = yi;
            this.fillBox(matrix, ibox);
          }
        }

        return true;
      } else {
        console.log('没有找到1x1相邻组件, 丢弃组件x1 （极端情况不保护）');
        return false;
      }
    },

    /** 辅助# 获取邻居 */
    getNears(matrix, box) {
      const { wSize } = this.config;
      // 左侧相邻
      if (
        box.xi > 0
        && matrix[box.yi][box.xi - 1] instanceof Object
        && matrix[box.yi][box.xi - 1].ws === 1
        && matrix[box.yi][box.xi - 1].hs === 1
      ) {
        return {
          point: { xi: box.xi - 1, yi: box.yi },
          nears: [matrix[box.yi][box.xi - 1], box],
        };
      }
      if (box.xi > 0 && matrix[box.yi][box.xi - 1] === 0) {
        return {
          point: { xi: box.xi - 1, yi: box.yi },
          nears: [box],
        };
      }
      // 右侧相邻
      if (
        box.xi < (wSize - 1)
        && matrix[box.yi][box.xi + 1] instanceof Object
        && matrix[box.yi][box.xi + 1].ws === 1
        && matrix[box.yi][box.xi + 1].hs === 1
      ) {
        return {
          point: { xi: box.xi, yi: box.yi },
          nears: [box, matrix[box.yi][box.xi + 1]],
        };
      }
      if (box.xi < (wSize - 1) && matrix[box.yi][box.xi + 1] === 0) {
        return {
          point: { xi: box.xi, yi: box.yi },
          nears: [box],
        };
      }
      return null;
    },

    /** 辅助# 空行填充 */
    fillBlank(matrix) {
      // 查找可交换组件
      const findBox = (items, { rxi, ryi }) => {
        for (let i = 0; i < items.length; i ++) {
          if (this.canBox(matrix, {
            xi: rxi, yi: ryi,
            ws: items[i].ws, hs: items[i].hs,
          })) return items[i]; 
        }
        return null;
      };

      /**
       * 因为填充是从上到下, 从左到右
       * 所以需要从最后一行右侧开始填充
       * 填充区块从往上开始查找并做位置交换
       */
      for (let ryi = (matrix.length - 1); ryi >= 0; ryi --) {
        // 忽略有组件的行
        if (! matrix[ryi].every(v => v === 0)) continue;
        // 获取当前行的所有空位
        let postions = Object.keys(matrix[ryi]).map(v => +v);
        // 获取当前行上所有组件（从下往上获取）
        let boxes = [];
        Object.keys(new Array(ryi).fill(0)).reverse().forEach((k) => {
          matrix[k].forEach((v) => {
            if (v instanceof Object) boxes.push(v);
          });
        });

        // console.log('检测空行', ryi, postions, boxes);
        
        do {
          // 弹出一个随机位置
          let rxi = postions.splice(Math.floor(Math.random() * postions.length), 1)[0];
          // 查找可交换的组件
          let box = findBox(boxes, { rxi, ryi });
          // 挪动组件位置到新的位置
          if (box) {
            // console.log('交换位置', box, rxi, ryi);
            this.clearBox(matrix, box);
            box.xi = rxi; box.yi = ryi;
            this.fillBox(matrix, box);
            break;
          }
        } while (postions.length > 0);
      }
    },

    /** 辅助# 是否填充 */
    canBox(matrix, { xi, yi, ws, hs } = {}) {
      // console.log('检测填充', `(${xi},${yi})`, `${ws}x${hs}`);
      for (let hi = 0; hi < hs; hi ++) {
        for (let wi = 0; wi < ws; wi ++) {
          if (
            matrix[yi + hi] === undefined
            || matrix[yi + hi][xi + wi] === undefined // 超出下标
            || matrix[yi + hi][xi + wi] !== 0 // 已被标记 1 和 {} 都是被占用
          ) {
            return false;
          }
        }
      }
      return true;
    },

    /** 辅助# 填充区域 */
    fillBox(matrix, box) {
      const { xi, yi, ws, hs } = box;
      // console.log('填充', `(${xi}, ${yi})`, `${ws}x${hs}`);

      // 更新组件的样式
      this.styleBox(box);

      // 查找并填充位置
      for (let hi = 0; hi < hs; hi ++) {
        for (let wi = 0; wi < ws; wi ++) {
          if (hi === 0 && wi === 0) {
            matrix[yi + hi][xi + wi] = box;
          } else {
            matrix[yi + hi][xi + wi] = 1;
          }
          // 加入坐标位置, 用于放大组件占用位置查找
          box.postions.push(`${xi + wi},${yi + hi}`);
        }
      }
    },

    /** 辅助# 清理区域 */
    clearBox(matrix, { xi, yi, ws, hs } = {}) {
      for (let hi = 0; hi < hs; hi ++) {
        for (let wi = 0; wi < ws; wi ++) {
          matrix[yi + hi][xi + wi] = 0;
        }
      }
    },

    /** 辅助# 封装计算位置及大小 */
    styleBox(box) {
      const {
        wBasis,
        hBasis,
        wGapSize,
        hGapSize,
      } = this.config;

      const top = (hBasis * box.yi) + (hGapSize * box.yi);
      const left = (wBasis * box.xi) + (wGapSize * box.xi);
      const width = (wBasis * box.ws) + (wGapSize * (box.ws - 1));
      const height = (hBasis * box.hs) + (hGapSize * (box.hs - 1));

      box.postions = [];
      box.styles = {
        top: `${top}px`,
        left: `${left}px`,
        width: `${width}px`,
        height: `${height}px`,
      };
    },

    /** 辅助# 删除组件 */
    delBox(ids) {
      ids.forEach(id => delete this.boxInfos[id]);
    },

    /** 辅助# 创建组件 */
    makeBox(info, { ws, hs } = {}) {
      // 生成标识
      let id = this.genUuid();
      // 建立关系
      this.boxInfos[id] = info;
      // 返回组件
      return {
        id,
        xi: 0,
        yi: 0,
        ws, hs,
        origin: { ws, hs },
        styles: {}, // 样式
        classes: [], // 类名
        postions: [], // 坐标集
      };
    },

    /** 辅助# 创建网格 */
    makeMatrix() {
      const { wSize, hSize } = this.config;
      return (new Array(hSize).fill(0)).map(() => {
        return (new Array(wSize).fill(0)).map(() => 0);
      })
    },

    /** 辅助# 获取坐标集 */
    getPostions({ xi, yi, ws, hs } = {}) {
      const postions = [];
      for (let hi = 0; hi < hs; hi ++) {
        for (let wi = 0; wi < ws; wi ++) {
          postions.push(`${xi + wi},${yi + hi}`);
        }
      }
      return postions;
    },

    /** 动画# 点击动画（逐渐放大） */
    animationActive(boxOrigin, boxTarget) {
      // 计算动画的名称
      const animationName = `anim-b${boxOrigin.top}${boxOrigin.left}${boxOrigin.width}${boxOrigin.height}-${boxTarget.top}${boxTarget.left}${boxTarget.width}${boxTarget.height}`;

      // 验证是否需要创建动画（放大动画）
      if (!this.animateCache[animationName]) {
        this.animateCache[animationName] = true;
        const iStyle = document.createElement('style');
        iStyle.innerHTML = `
          .${animationName} {
            animation-name: ${animationName};
            animation-duration: 1s;
            animation-iteration-count: 1;
            animation-timing-function: ease;
          }
          @keyframes ${animationName} {
            0% {
              width: ${boxOrigin.width};
              height: ${boxOrigin.height};
              left: ${boxOrigin.left};
              top: ${boxOrigin.top};
            }
            100% {
              width: ${boxTarget.width};
              height: ${boxTarget.height};
              left: ${boxTarget.left};
              top: ${boxTarget.top};
            }
          }
        `;
        document.head.appendChild(iStyle);
      }
      return animationName;
    },

    /** 动画# 移动动画（逐渐放大） */
    animationMove(boxOrigin, boxTarget) {
      // 计算动画的名称
      const animationName = `anim-m${boxOrigin.top}${boxOrigin.left}${boxOrigin.width}${boxOrigin.height}-${boxTarget.top}${boxTarget.left}${boxTarget.width}${boxTarget.height}`;

      // 验证是否需要创建动画（放大动画）
      if (!this.animateCache[animationName]) {
        this.animateCache[animationName] = true;
        const iStyle = document.createElement('style');
        iStyle.innerHTML = `
          .${animationName} {
            animation-name: ${animationName};
            animation-duration: 1s;
            animation-iteration-count: 1;
            animation-timing-function: ease-out;
          }
          @keyframes ${animationName} {
            0% {
              width: ${boxOrigin.width};
              height: ${boxOrigin.height};
              left: ${boxOrigin.left};
              top: ${boxOrigin.top};
            }
            20% {
              transform: scale(0.5);
              opacity: 0.5;
            }
            60% {
              transform: scale(0.5);
              left: ${boxTarget.left};
              top: ${boxTarget.top};
              opacity: 0.5;
            }
            100% {
              width: ${boxTarget.width};
              height: ${boxTarget.height};
              left: ${boxTarget.left};
              top: ${boxTarget.top};
              opacity: 1;
            }
          }
        `;
        document.head.appendChild(iStyle);
      }
      return animationName;
    },

    /** 动画# 出现动画 */
    animationShow() {
      const animationName = 'anim-appear';
      if (!this.animateCache[animationName]) {
        this.animateCache[animationName] = true;
        const iStyle = document.createElement('style');
        iStyle.innerHTML = `
          .anim-appear {
            animation-name: anim-appear;
            animation-duration: 600ms;
            animation-iteration-count: 1;
            animation-timing-function: linear;
          }
          @keyframes anim-appear {
            0% {}
            100% {
              transform: rotateX(180deg) scaleY(-1);
            }
          }
        `;
        document.head.appendChild(iStyle);
      }
      return animationName;
    },
  }
};
</script>

<style>
.box-wrapper {
  flex: 1;
  width: 100%;
  height: 100%;
  position: relative;
  box-sizing: border-box;
}
</style>